---
title: Add navigation
description: In this chapter, learn how to add navigation to the Expo app.
hasVideoLink: true
---

import { Collapsible } from '~/ui/components/Collapsible';
import { ContentSpotlight } from '~/ui/components/ContentSpotlight';
import { FileTree } from '~/ui/components/FileTree';
import { ProgressTracker } from '~/ui/components/ProgressTracker';
import { Step } from '~/ui/components/Step';
import { CODE } from '~/ui/components/Text';
import { VideoBoxLink } from '~/ui/components/VideoBoxLink';

In this chapter, we'll learn Expo Router's fundamentals to create stack navigation and a bottom tab bar with two tabs.

<VideoBoxLink videoId="8336fcFV_T4" title="Watch: Adding navigation in your universal Expo app" />

## Expo Router basics

Expo Router is a file-based routing framework for React Native and web apps. It manages navigation between screens and uses the same components across multiple platforms. To get started, we need to know about the following conventions:

- **app directory**: A special directory containing only routes and their layouts. Any files added to this directory become a screen inside our native app and a page on the web.
- **Root layout**: The **app/\_layout.tsx** file. It defines shared UI elements such as headers and tab bars so they are consistent between different routes.
- **File name conventions**: _Index_ file names, such as **index.tsx**, match their parent directory and do not add a path segment. For example, the **index.tsx** file in the **app** directory matches `/` route.
- A **route** file exports a React component as its default value. It can use either `.js`, `.jsx`, `.ts`, or `.tsx` extension.
- Android, iOS, and web share a unified navigation structure.

> The above list is enough for us to get started. For a complete list of features, see [Introduction to Expo Router](/router/introduction/).

<Step label="1">

## Add a new screen to the stack

Let's create a new file named **about.tsx** inside the **app** directory. It displays the screen name when the user navigates to the `/about` route.

```tsx app/about.tsx|collapseHeight=300
import { Text, View, StyleSheet } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>About screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    color: '#fff',
  },
});
```

Inside **app/\_layout.tsx**:

1. Add a `<Stack.Screen />` component and an `options` prop to update the title of the `/about` route.
2. Update the `/index` route's title to `Home` by adding `options` prop.

{/* prettier-ignore */}
```tsx app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    /* @tutinfo Wrap screens inside <CODE>Stack</CODE> navigator. Add <CODE>options</CODE> prop to <CODE>/index</CODE> route. Add another <CODE>Stack.Screen</CODE> component to add <CODE>/about</CODE> route. */
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="about" options={{ title: 'About' }} />
    </Stack>
    /* @end */
  );
}
```

<Collapsible summary={<>What is a <CODE>Stack</CODE>?</>}>

A stack navigator is the foundation for navigating between different screens in an app. On Android, a stacked route animates on top of the current screen. On iOS, a stacked route animates from the right. Expo Router provides a `Stack` component to create a navigation stack to add new routes.

</Collapsible>

</Step>

<Step label="2">

## Navigate between screens

We'll use Expo Router's `Link` component to navigate from the `/index` route to the `/about` route. It is a React component that renders a `<Text>` with a given `href` prop.

1. Import the `Link` component from `expo-router` inside **index.tsx**.
2. Add a `Link` component after `<Text>` component and pass `href` prop with the `/about` route.
3. Add a style of `fontSize`, `textDecorationLine`, and `color` to `Link` component. It takes the same props as the `<Text>` component.

{/* prettier-ignore */}
```tsx app/index.tsx|collapseHeight=300
import { Text, View, StyleSheet } from 'react-native';
/* @tutinfo Import <CODE>Link</CODE> component from <CODE>expo-router</CODE>. */ import { Link } from 'expo-router'; /* @end */

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home screen</Text>
      /* @tutinfo Add <CODE>Link</CODE> component after <CODE>Text</CODE> component and pass the <CODE>href</CODE> prop with <CODE>/about</CODE> route. */
      <Link href="/about" style={styles.button}>
        Go to About screen
      </Link>
      /* @end */
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: '#fff',
  },
  /* @tutinfo Add the style of <CODE>fontSize</CODE>, <CODE>textDecorationLine</CODE>, and <CODE>color</CODE> to <CODE>Link</CODE> component. */
  button: {
    fontSize: 20,
    textDecorationLine: 'underline',
    color: '#fff',
  },
  /* @end */
});
```

Let's take a look at the changes in our app. Click on `Link` to navigate to the `/about` route:

<ContentSpotlight file="tutorial/01-navigating-between-screens.mp4" />

</Step>

<Step label="3">

## Add a not-found route

When a route doesn't exist, we can use a `+not-found` route to display a fallback screen. This is useful when we want to display a custom screen when navigating to an invalid route on mobile instead of crashing the app or display a _404_ error on web. Expo Router uses a special **+not-found.tsx** file to handle this case.

1. Create a new file named **+not-found.tsx** inside the app directory to add the `NotFoundScreen` component.
2. Add `options` prop from the `Stack.Screen` to display a custom screen title for this route.
3. Add a `Link` component to navigate to the `/` route, which is our fallback route.

{/* prettier-ignore */}
```tsx app/+not-found.tsx
import { View, StyleSheet } from 'react-native';
import { Link, Stack } from 'expo-router';

export default function NotFoundScreen() {
  return (
    <>
      /* @tutinfo Add <CODE>Stack.Screen</CODE> component with <CODE>options</CODE> prop to update the title of <CODE>+not-found</CODE> route. */
      <Stack.Screen options={{ title: 'Oops! Not Found' }} />
      /* @end */
      <View style={styles.container}>
        /* @tutinfo Adding a fallback route allows the user to navigate to that screen. Here, the fallback route is Home screen. */
        <Link href="/" style={styles.button}>
          Go back to Home screen!
        </Link>
        /* @end */
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    justifyContent: 'center',
    alignItems: 'center',
  },

  button: {
    fontSize: 20,
    textDecorationLine: 'underline',
    color: '#fff',
  },
});
```

To test this, navigate to `http:localhost:8081/123` URL in the web browser since it is easy to change the URL path there. The app should display the `NotFoundScreen` component:

<ContentSpotlight file="tutorial/02-not-found-route.mp4" />

</Step>

<Step label="4">

## Add a bottom tab navigator

At this point, the file structure of our **app** directory looks like the following:

<FileTree
  files={[
    ['app/_layout.tsx', 'Root layout'],
    ['app/index.tsx', "matches route '/'"],
    ['app/about.tsx', "matches route '/about'"],
    ['app/+not-found.tsx', 'matches route any 404 route'],
  ]}
/>

We'll add a bottom tab navigator to our app and reuse the existing Home and About screens to create a tab layout (a common navigation pattern in many social media apps like X or BlueSky). We'll also use the stack navigator in the Root layout so the `+not-found` route displays over any other nested navigators.

1. Inside the **app** directory, add a **(tabs)** subdirectory. This special directory is used to group routes together and display them in a bottom tab bar.
2. Create a **(tabs)/\_layout.tsx** file inside the directory. It will be used to define the tab layout, which is separate from Root layout.
3. Move the existing **index.tsx** and **about.tsx** files inside the **(tabs)** directory. The structure of **app** directory will look like this:

<FileTree
  files={[
    ['app/_layout.tsx', 'Root layout'],
    ['app/+not-found.tsx', 'matches route any 404 route'],
    ['app/(tabs)/_layout.tsx', 'Tab layout'],
    ['app/(tabs)/index.tsx', "matches route '/'"],
    ['app/(tabs)/about.tsx', "matches route '/about'"],
  ]}
/>

Update the Root layout file to add a `(tabs)` route:

{/* prettier-ignore */}
```tsx app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      /* @tutinfo This is how a tab navigator is nested inside a stack navigator, especially when the Root layout is composed of a parent stack navigator. We're also setting the <CODE>headerShown</CODE> option to <CODE>false</CODE> to hide the header for the tab navigator. Otherwise, there will be two headers displayed on each screen. */
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      /* @end */
    </Stack>
  );
}
```

Inside **(tabs)/\_layout.tsx**, add a `Tabs` component to define the bottom tab layout:

{/* prettier-ignore */}
```tsx app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      /* @tutinfo <CODE>Tabs.Screen</CODE> component works in a similar way and accepts same props as <CODE>Stack.Screen</CODE> component. The only difference is the navigation pattern on the device. */
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="about" options={{ title: 'About' }} />
      /* @end */
    </Tabs>
  );
}
```

Let's take a look at our app now to see the new bottom tabs:

<ContentSpotlight
  alt="Bottom tab navigator shown on all platforms"
  src="/static/images/tutorial/03-bottom-tab-navigator.png"
  className="max-w-[720px]"
/>

</Step>

<Step label="5">

## Update bottom tab navigator appearance

Right now, the bottom tab navigator looks the same on all platforms but doesn't match the style of our app. For example, the tab bar or header doesn't display a custom icon, and the bottom tab background color doesn't match the app's background color.

Modify the **(tabs)/\_layout.tsx** file to add tab bar icons:

1. Import `Ionicons` icons set from [`@expo/vector-icons`](/guides/icons/#expovector-icons) &mdash; a library that includes popular icon sets.
2. Add the `tabBarIcon` to both the `index` and `about` routes. This function takes `focused` and `color` as params and renders the icon component. From the icon set, we can provide custom icon names.
3. Add `screenOptions.tabBarActiveTintColor` to the `Tabs` component and set its value to `#ffd33d`. This will change the color of the tab bar icon and label when active.

{/* prettier-ignore */}
```tsx app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
/* @tutinfo Import <CODE>Ionicons</CODE> icon set.*/
import Ionicons from '@expo/vector-icons/Ionicons';
/* @end */

export default function TabLayout() {
  return (
    <Tabs
      /* @tutinfo There are many <CODE>screenOptions</CODE> we can use to customize the tab bar. We're changing the active tab color here to custom value which we will also use later in our app. */
      screenOptions={{
        tabBarActiveTintColor: '#ffd33d',
      }}
      /* @end */
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, focused }) => (
            /* @tutinfo The <CODE>focused</CODE> param allows us to change a tab's icon and label behavior when it is active and inactive.*/
            <Ionicons name={focused ? 'home-sharp' : 'home-outline'} color={color} size={24} />
            /* @end */
          ),
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: 'About',
          tabBarIcon: ({ color, focused }) => (
            /* @tutinfo */
            <Ionicons name={focused ? 'information-circle' : 'information-circle-outline'} color={color} size={24}/>
            /* @end */
          ),
        }}
      />
    </Tabs>
  );
}
```

Let's also change the background color of the tab bar and header using `screenOptions` prop:

```tsx app/(tabs)/_layout.tsx
<Tabs
  screenOptions={{
    tabBarActiveTintColor: '#ffd33d',
    headerStyle: {
      backgroundColor: '#25292e',
    },
    headerShadowVisible: false,
    headerTintColor: '#fff',
    tabBarStyle: {
      backgroundColor: '#25292e',
    },
  }}
>
```

In the above code:

- The header's background is set to `#25292e` using the `headerStyle` property. We have also disabled the header's shadow using `headerShadowVisible`.
- `headerTintColor` applies `#fff` to the header label
- `tabBarStyle.backgroundColor` applies `#25292e` to the tab bar

Our app now has a custom bottom tabs navigator:

<ContentSpotlight
  alt="Custom bottom tabs navigator"
  src="/static/images/tutorial/04-tab-navigator-complete.png"
  className="max-w-[720px]"
/>

</Step>

## Summary

<ProgressTracker
  currentChapterIndex={1}
  name="GET_STARTED"
  summary="We've successfully added a stack and a tab navigator to our app."
  nextChapterDescription="In the next chapter, we'll learn how to build the app's first screen."
  nextChapterTitle="Build your app's first screen"
  nextChapterLink="/tutorial/build-a-screen"
/>
